閱讀本篇文章前,仔細想想看
- 還記得存取修飾模式(Access Modifiers)有哪些嗎?
- 你有想過
private
除了類別成員與類別的靜態屬性與方法外,還有哪些地方可以使用呢?(提示:今天的文章標題)
今天的標題筆者亂打 XD,但 Singleton Pettern 單例模式 —— 也就是今天要講到的 Design Pattern 的應用之一,確實在中文翻譯上很難翻得好聽,因為 Singleton 本身就帶有單身漢的意思。通常會翻譯成單例、單子或獨體模式,讀者如果上網查不外乎會看到這些翻譯。
不過在講到今天的主角前,ㄧ樣先從過往學到的東西出發~
以下,正文開始~
標題很直觀,就是將類別裡的建構子加上 private
這個修飾模式就可以了。(今天重點下得倒挺快的,之前為了鋪陳還要講一大堆東西)
重點 1. 私有建構子 Private Constructor
私有建構子 —— 顧名思義,就是將類別的建構子設定成
private
模式。若某類別C
,其建構子設定成private
模式,如下:其中,因為
C
的建構子被設定成private
模式了,因此我們不能夠從該類別C
進行建構物件的動作:new C(/* 參數 */)
在類別的外部不能被使用
讀者云:“這是叫我們怎麼使用類別啊?”
是的,一但將類別建構子私有化不能在外面建構物件!(沒聽過黨產嗎?—— “反對的舉手” ... “沒有!” ... “沒有!” ... “沒有!” ... “好!一致通過!”)
(以下是簡單的私有建構子的範例;TypeScript 檢測結果如圖一)
圖一:就連建構子都被封裝到類別裡面去了(這句話筆者覺得特性跟克萊因瓶 (Klein Bottle)很像 XD)
沒看過私有建構子的讀者肯定覺得莫名其妙,不過筆者繼續推展下去。
筆者一樣開始引導讀者的思路:
通常會把類別的建構開口給封住的原因會是什麼呢?
於是有幾個想法可能會冒出來:
Math
物件,寫一些由類別本身提供的功能嗎?當然,第 4 點的想法,用類別本身的靜態屬性寫出一系列的功能,仿造 JS 裡的 Math
物件是可以的喔!不過就看個人需求要不要這麼做~
今天要講的主題是單例模式 —— 以上列出的幾個想法,隱隱約約產出了幾個重點關鍵字:『 靜態屬性與方法 』以及『 不是拿來建構物件用 』
筆者就直接從中點出單例模式的特點:若某類別採取單例模式,則該類別產出的物件會是全域裡面的唯一個體。
以下是單例模式的示範程式碼:
筆者簡單敘述一下到底這是在做什麼:
SingletonPerson
為使用單例模式的類別。單例模式的實踐方法 —— 必須具備私有建構子,防止外部的人私自建構更多該類別的物件
SingletonPerson
的建構子裡面有三個成員變數:name
、age
以及 hasPet
;儘管是 public
模式,但是不能夠被覆寫,因為也被標註為 readonly
SingletonPerson
有一個私有靜態屬性名為 Instance
,存放的是單例模式下:唯一一個被建構的物件;當程式碼讀進去的時候,立即被建構起來。SingletonPerson
有一個公用靜態方法名為 getInstance
,其實就是把唯一的建構出來物件叫出來(另外讀者可以注意到的一點是,Instance
對應型別與 getInstance
方法輸出的對應型別都是 SingletonPerson
,有關於類別也是型別的一種 —— 這點筆者還沒清楚說明過,後續會再補足)
我們可以用 SingletonPerson.getInstance()
的方式去呼叫 SingletonPerson
唯一建構出來的物件。(以下程式碼編譯並使用 node
執行過後結果如圖二)
圖二:我們可以從 getInstance
類別靜態方法取得單例模式下建構出來的唯一物件呢
重點 2. 基本的單例模式實踐 Singleton Pattern
若某類別
SingletonC
實踐單例模式,則必須符合:
SingletonC
的建構子函式為private
模式SingletonC
必須要有私有靜態屬性專門存放單例模式下的唯一物件(該物件又被稱為 Singleton,或單子),習慣上該靜態屬性的名稱為Instance
SingletonC
必須要有公用靜態方法負責把單子回傳出來,是唯一一個取得單子的途徑,習慣上該靜態方法的名稱為getInstance
基本上,從剛剛的範例應該可以推測出單例模式可以解決到的問題,最明顯的莫過於:
確保該物件(單子)在任何地方的單一性,並且針對該物件提供統一的成員方法
比如說,我們可能會讓專案的設定檔(Configuration File)成為全域裡 —— 唯一的一種物件。
另外,在任何語言 —— 尤其是多執行緒(Multithreaded)的環境下,有可能會出現同時有多個執行緒呼叫到 new SingletonInstance
,因此產生了兩個以上的單子 —— 這是不合理的行為,所以如果讀者轉換到其他語言,想要實踐單例模式,必須注意有沒有發生的可能性(大部分都會,現在幾乎都是多執行緒的環境了)。
單例模式事實上是可以被繼承的 —— 讀者可能想說:“建構子都被鎖住了,繼承後的子類別頂多也需要父類別的建構子在 protected
模式下才能夠覆寫啊?”
StackOverflow 裡確實有人提議 —— 將 constructor
設定為 protected
模式,子類別就可以覆寫父類別的建構子函式。
不過筆者查閱了以下這本物件導向的設計模式原著翻譯本(如圖三)。
圖三:當時 OOP 圈熱門的 Gang of Four —— 四人幫整理出的 23 種不同設計模式匯集在這本書裡
裡面寫道對於單例類別的繼承的用意是:
(前面有兩點筆者略過)
操作和內部結構仍有內部空間。Singleton 類別可被繼承,我們可用其子類別的物件個體輕鬆地設定應用程式組態,也可以在執行期動態選擇要用哪一種 Singleton 子類別的物件個體。
允許不定個數的物件個數。同理,...(筆者後面略過)
這裡作者們(因為是四人幫)已經闡明:
繼承的用意在於擴充單子個體的功能,並沒有叫你覆寫父類別的建構子
所以那些網路上跟讀者講說:“你可以使用 protected
模式的建構子,繼承過後進行覆寫父類別的建構子的動作” —— 被筆者俗稱網路偏方,嚴重模糊焦點甚至講錯觀念叫做網路謠言(我們知道的假消息、假新聞等等) —— 就好像聽說手機的電磁波會讓人致癌,如果是這樣,那全世界的人都 GG 了,因為衛星發出的電磁波散佈在各地,穿梭人體來去自如!
筆者的目的是要讓讀者知道:有些資訊必須去求證,就算是吹毛求疵也好,不能單純只有 Gxxgle 來 Gxxgle 去(聽說 Gxxgle 是被註冊過的商標?)—— 結果被灌到錯誤的資訊,而網路戰的由來,其中一種就是去 Exploit 一個群體的資訊落差然後再進行內部分化。(內容超展開)
儘管我們在開發的過程中遇到很多 Bug 必須上網查詢解決,但是最重要的是 —— 任何可以求證資訊的機會不要放過。
另外,時常還有人自認為(筆者就是其中之一,看完原著的書反省了三天三夜,丟臉到膝蓋都不見了):
(以下是錯誤觀念)
你對單例模式的類別繼承過後,因為有了複數個子類別等同於違反了單一物件個體建構的原則,也就是 Singleton 本身的意涵 —— 要維持單一物件的狀態
原著的意思是:
“單例類別被繼承的目的:除了可以擴充個體的功能外,多個子類別不同的單例個體可以抽換運用”
這是一種更彈性的作法。請再重看第三點被筆者特別匡起來的部分就可以知道:
- 操作和內部結構仍有內部空間。Singleton 類別可被繼承,我們可用其『 子類別的物件個體輕鬆地設定應用程式組態,也可以在執行期動態選擇要用哪一種 Singleton 子類別的物件個體 』。
讀者可以另外上網查有關於某些開發者認為單例類別不能繼承這一個觀點 —— 那些都是錯的、亦或者是根本連原著都沒看過。其中有一篇 Medium 文章(筆者不貼,讀者自己去找,而且不只是一些 Medium 文章,部落格等等可以找),它就這樣大膽闡述:
Notice how the constructor is made private to eliminate the ability to make an instance of the class outside the Singleton class. If you look carefully you will notice that the class is made final as well, this to indicate that the class cannot be inherited (private constructor also inhibit inheritance).
以上簡單翻譯是:『 注意到因為單例類別的建構子已經被私有化,代表它是不能被繼承的 』
請讀者以後看到類似的話,果斷對那篇文章說不。(但你不需要防狼噴霧,或者是可以選擇請它進 OOP 設計模式勞改營)
重點 3. 單例模式的意義與注意事項
確保某物件(單子)在任何地方的單一性,並且針對該物件提供統一的成員方法。
在多執行緒下的環境,必須確保單例模式下的類別,不會因為兩個執行緒以上同時讀到類別裡 —— 建構物件的表達式而違反單例模式的初衷。
另外,對單例模式的類別進行繼承的動作,並不違反單例模式的初衷。在單例模式下運用繼承的目的有二:
- 子類別可以擴充該個體的功能(可能擴充靜態方法等)
- 多個子類別的單例物件可以在程式中隨時抽換
最後,建議不要將單例模式的建構子函式設定成
protected
模式,因為這並不是設計模式原著主張的東西,而是網路偏方。
以下附上書籍寫的內容,確保筆者沒有從其他消息來源造假,但筆者鼓勵讀者買書來看 XD。(如圖四)
圖四:《物件導向設計模式 —— 可再利用物件導向軟體之要素》—— Page. 146 節選內容
(這真的就是維基翻譯出來的名稱,不要討伐筆者 XD)
由於單例模式是在剛初始化類別的時候就順便將單子給建構好。有時候會遇到單子建構過程中會耗費龐大的資源;另外,如果剛開始也不急著去建構單子物件亦或者是單子物件的需求性不高 —— 搞不好在整個程式的運轉當中也不需要單子物件,所以建構好的單子也會被視為浪費資源的可能性的話,那麼我們還有單例模式的變體 —— 懶漢模式。
其實懶漢模式的概念很簡單 —— 第一次呼叫到 SingletonClass.getInstance
這個靜態方法時,到時候再建造就好了,於是就出現了以下的程式碼。
你可以看到我們的 Instance
的型態為 null
型別的 union
的複合型別 —— 代表剛宣告單例模式的類別時,先不要把單子建構出來,而是用 null
值來代替。
等開發者第一次呼叫到 getInstance
靜態方法才正式把單子給建構好。
這也是一種 Lazy Initialization 的應用概念啊!
重點 4. 單例模式變體 —— 使用延遲初始化技巧
如果碰到單子不急著被建構出來的情形,可以採取呼叫
getInstance
方法才建構單子的模式。
今天主要講解跟類別有關的設計模式的應用,單例模式其實很好理解,就是確保物件的單一性。
另外就是:闢謠!闢謠!闢謠!—— 這點非常重要!
下一篇又會回歸要講解類別的型別推論與註記機制(Type Annotation & Inference),這一部分會跟《前線維護》系列調性很像~但還是非常重要,筆者很雞婆的還是得說:“因為這是 TypeScript 主打的 Feature 啊!”
嗨~ 這裡有個疑問:
因為我沒接觸過多執行緒,所以不確定多執行緒的環境下會怎麼處理。
但就我對 JS 的理解,在 JS 的 class 中,static
應該是在建立 class 當下就已經固定了。
照理說應該不會多 new
幾次,而影響到 instance。
所以好奇說,這裡的 constructor
有其他原因,必定得將其設置為 private
嗎?
謝謝。